組込み現場の「C++」プログラミング 明日から使える徹底入門

高木 信尚(株式会社クローバーフィールド

4.5 現実の処理系の標準準拠度

C++の標準規格が制定されてから10年になります.しかし,実際に我々が扱わなければならないコンパイラのうち標準規格に完全に合致しているものは,実質的には皆無です.そのため,標準C++に沿ってプログラミングを行うといっても,コンパイラごとにある程度の方言やバグを覚悟しなければなりません.もっとも,標準規格に準拠していない箇所の多くには“傾向”があり,その大部分はふだん気づくこともないようなものです.

ここでは,標準準拠度の低い処理系によく見られる問題点を示すことにします.同時に,問題点の回避方法も示します.移植性の高いプログラムを書くためには,これらに十分留意する必要があります.

4.5.1 テンプレートに関する問題点

標準規格を満たす処理系がほとんどないもの代表格が「移出」です.exportといったほうがわかりやすいかもしれません.これは,ある翻訳単位で定義したテンプレートを,他の翻訳単位から参照できるようにすることです.通常,テンプレートは,ヘッダファイルの中で関数や静的データメンバーの定義まですべて記述します.そして,実際のプログラムで使われたものだけが具現化されることになります.しかし,そうしたテンプレートの定義は,ヘッダファイルの中に記述するのではなく,できれば外部定義にしたいところです.それを実現するのが「移出」というわけです.

【foo.h - ヘッダファイル】

// foo.h
template <typename T>
T foo(const T& arg);

【foo.cpp】

// foo.cpp
export template <typename T>
T foo(const T& arg)
{
     …
}

「移出」をサポートしている処理系は,筆者が知るかぎり,Comeau C/C++だけです.こうしたことから,テンプレートの移出はあきらめざるをえないのが実情です.

移出ほど極端ではないにせよ,テンプレートに関しては,ほかにも遭遇しやすい問題がいくつかあります.テンプレートの部分特殊化がそうです.

たとえば,次のようなものです.

template <bool F, typename T1, typename T2>
struct select_type
{
    typedef T1 type;
};
template <typename T1, typename T2>
struct select_type<false, T1, T2>
{
    typedef T2 type;
};
select_type<true, int, long>  a; // ← a aはint型 
select_type<false, int, long> b; // ← bはlong型 

テンプレートの部分特殊化はジェネリックプログラミングには不可欠の仕様ですが,残念ながら処理系の問題に遭遇しやすいことも事実です.ただ,最近ではまったく利用できない処理系はまれで,若干記述を変更すれば,期待どおりに動いてくれることのほうが多いようです.そのため,この問題を回避するには,条件付きコンパイルを用いて個別に対応する必要があります.

たとえば,次のようにです.

#if 処理系A
template <bool F, typename T1, typename T2>
struct select_type
{
private:
    template <bool>
    struct if_true { typedef T1 type; }
    template <>
    struct if_true<false> { typedef T2 type; }
public:
    typedef typename if_true<F>::type type;
};
#elif 処理系B
 …
#else
template <bool F, typename T1, typename T2>
struct select_type
{
    typedef T1 type;
};
template <typename T1, typename T2>
struct select_type<false, T1, T2>
{
    typedef T2 type;
};
#endif

古い処理系の中には,省略時テンプレート実引数をサポートしていないものもあります.

具体的には,次のような場合です.

template <class T, class Allocator = allocator<T> >
class vector;

上の「= allocator<T>」の部分がコンパイルできない処理系があるのです.その場合,テンプレート実引数の省略ができませんから,つねに,「vector<int, allocator<int> >」のようにフルネームで記述する必要がありました.さすがにこれでは不便ですので,適当なtypedef名を定義するなどして問題を回避する必要があります.

また,これも古い処理系の話ですが,メンバーテンプレートが使えない場合がありました.先ほどのvectorクラステンプレートをもう一度持ち出すと,次のようなメンバーが使えないのです.

template <class T, class Allocator = allocator<T> >
class vector
{
public:
    template <class InputIterator>
    vector(InputIterator first, InputIterator last, 
           const Allocator& allocator = Allocator());
     …
};

そうした処理系では,多かれ少なかれ機能を制限せざるをえません.そして,メンバー関数をテンプレートにするのではなく,必要なものだけ多重定義することで問題を回避する必要があります.

ほかにもテンプレートに関連する問題はありますが,遭遇する機会は少ないと思いますので,ここでは割愛することにします.

4.5.2 名前空間に関する問題点

名前空間に関する問題には,「実引数依存の名前検索」に関するものがあります.実引数依存の名前検索については,「1.3.5 名前の衝突を防ぐ『名前空間』」を参照してください.

問題のある処理系には2種類あります.問題が大きいものは,実引数依存の名前検索がまったく働きません.別のものでは,多重定義した演算子についてのみ実引数依存の名前検索が働きます.多重定義した演算子についても実引数依存の名前検索が働かない場合には,必要に応じてusing宣言を行うか,演算子の多重定義は大域的名前空間で行うしかありません.しかし,そうした処理系は多くはありませんので,現実的には後者(多重定義した演算子についてのみ実引数依存の名前検索が働くもの)だけを相手にしてもよいでしょう.この場合には,特定の名前空間内で宣言した関数を呼び出す際は,必ずusing宣言を行うか,明示的に名前空間を指定することで容易に回避することができます.

4.5.3 例外処理に関する問題点

例外処理に関する問題には,「例外指定」に関するものがあります.この問題の場合にも,問題の程度が何種類かあります.最も問題が大きいものでは,例外指定は記述できるものの,まったく機能しません.もう少しましなものでは,throw(),すなわち例外を送出しない指定だけが機能し,それ以外は機能しません.そして,多くの処理系では,関数へのポインタに対する例外指定が無視されます.

void (*f)() throw();
void (*g)();
int main()
{
    g = f; // ← OK! 
    f = g; // ← エラー! 
}

本来であれば上記のようになるべきですが,多くの処理系では,エラーになるべきところでエラーにならないのです.これを回避するには,エラー検出をコンパイラに任せるのではなく,自分の目で確認する以外にはなさそうです.

以上のほかにも問題点が存在しますが,多くの場合,遭遇する機会がほとんどないか,容易に回避できるため,ここではあえて紹介しません.C++には移植性に関する問題点がありますが,いますぐ複数の処理系に移植する必要がないのであれば,まずは標準規格に沿った実装を心掛けることをお勧めします.処理系の標準準拠度は年々向上しているので,現時点では移植性に難がある仕様でも,実際に移植の必要性が生じる頃には,問題が解消されている可能性は十分にあるからです.